using System;
using System.Collections;
using MizExceptions;
namespace Model
{
	
	/// <summary>
	/// The primary enitity class that models all services available and representing the user.
	/// This is the central basis of the model.
	/// </summary>
	public class UserManager
	{
		UserIdentity currentUserID ;
		
		public delegate void TransactionEventHandler(object sender, MizEventArgs.ActionTransactionUpdate ev);
		public event TransactionEventHandler TransactionAction;

		public delegate void ExternalAccountEventHandler(object sender, MizEventArgs.ActionExternalAccountUpdate ev);
		public event ExternalAccountEventHandler ExternalAccountAction;

        /// <summary>
        /// Delegate used for the event when service is unavailable
        /// </summary>
        /// <param name="sender">The object that raised the event</param>
        /// <param name="ev">The parameters of the event</param>
        public delegate void ServiceDisconnectedEventHandler(object sender, MizEventArgs.ServiceDisconnectEventArgs ev);
        /// <summary>
        /// Event fired when the service is unavailable
        /// </summary>
        public event ServiceDisconnectedEventHandler ServiceDisconnectedAction;

		/// <summary>
		/// Array List of Transactions that need to be approved
		/// </summary>
		// private ArrayList TransactionNotificationList;
		/// <summary>
		/// All services that are required by the model, primarily data persistance, should be 
		/// </summary>
		public IRemoteService remoteConnection;
		/// <summary>
		/// Array of transactions that the user is involved in
		/// </summary>
		public ArrayList TransactionList; 
		/// <summary>
		/// Array of User's External Accounts
		/// </summary>
		public ArrayList ExternalAccountList;	
		
		/// <summary>
		/// Initializes the User
		/// </summary>
		/// <param name="iniremoteConnection">All services that are required by the model, primarily data persistance, should be offered by this class</param>
		public UserManager(IRemoteService iniremoteConnection)
		{
			this.remoteConnection=iniremoteConnection;
			//currentUserID = null;
		}
		
		/// <summary>
		/// Return all external accounts of the user
		/// </summary>
		/// <returns></returns>
		public string[] GetExternalAccounts()
		{
			ArrayList accountList = this.remoteConnection.GetAllUserExternalAccounts(this.currentUserID);
			string [] accountNames = new string[accountList.Count];
			for (int i = 0; i < accountList.Count; i++)
			{
				if (accountList[i] is Model.BankAccount)
					accountNames[i] = ((BankAccount)accountList[i]).AccountName;
				else if (accountList[i] is Model.Cash)
					accountNames[i] = ((Cash)accountList[i]).AccountName;
				else if (accountList[i] is Model.CreditCard)
					accountNames[i] = ((CreditCard)accountList[i]).AccountName;
			}

			for (int i = 0; i < accountNames.Length; i++)
				Console.WriteLine(accountNames[i]);
			
			return accountNames;
		}

		/// <summary>
		/// will update balance of the account depending on type of transacion.
		/// If it is a revenue, and the user is a consumer, then the account is updated.
		/// If it is an expense, and the user is a payee, then the account is updated.		/// 
		/// </summary>
		/// <param name="newTx">The transaction to update to</param>
		/// <param name="account"></param>
		public  void UpdateExternalAccountBalance(Transaction newTx)
		{
			// initialize the amount currently associated with the tx
			decimal amountInvolved = newTx.InvolvedUserAmount(currentUserID.Name);
			ExternalAccount newAccount = GetExternalAccountFromName(newTx.SourceExternalAccount);

			// initialize the amount that was previosly associated with the transaction
			decimal amountInvolvedPrevious=0m; 
			// oldAccount should not be null - cannot send null to database
			ExternalAccount oldAccount = newAccount; // TODO: clone



			// what kind of update needs to be done
			bool isDepositUpdate = newTx.Type.ToString() == "Revenue" && newTx.IsConsumer(currentUserID.Name);
			bool isWithrdawUpdate = newTx.Type.ToString() == "Expense" && newTx.IsPayee(currentUserID.Name);

			if (isDepositUpdate || isWithrdawUpdate)
			{
				if (isDepositUpdate)
				{
					newAccount.Deposit(amountInvolved);	// do the current balance
				}

				if (isWithrdawUpdate)
				{
					newAccount.Withdraw(amountInvolved); // do the current balance
				}

				UpdateExternalAccount(oldAccount,newAccount);


			}
		}

		/// <summary>
		/// Rolls back the account balance to what it was before a transaction was committed.
		/// </summary>
		/// <param name="oldTx"></param>
		private void revertExternalAccountBalance(Transaction oldTx)
		{
			// initialize the amount currently associated with the tx
			decimal amountInvolved = oldTx.InvolvedUserAmount(currentUserID.Name);
			ExternalAccount oldAccount = GetExternalAccountFromName(oldTx.SourceExternalAccount);

			// initialize the amount that was previosly associated with the transaction
			decimal amountInvolvedPrevious=0m; 
			// oldAccount should not be null - cannot send null to database
			ExternalAccount newAccount = oldAccount; // TODO: clone



			// what kind of update needs to be done
			bool toUndoDeposit = oldTx.Type.ToString() == "Revenue" && oldTx.IsConsumer(currentUserID.Name);
			bool toUndoWithdraw = oldTx.Type.ToString() == "Expense" && oldTx.IsPayee(currentUserID.Name);

			if (toUndoDeposit || toUndoWithdraw)
			{
				if (toUndoDeposit)
				{
					newAccount.Withdraw(amountInvolved);	// do the current balance
				}

				if (toUndoWithdraw)
				{
					newAccount.Deposit(amountInvolved); // do the current balance
				}

				UpdateExternalAccount(oldAccount,newAccount);


			}

		}
		public void UpdateExternalAccount(ExternalAccount oldAcc, ExternalAccount newAcc)
		{
			remoteConnection.UpdateExternalAccount(currentUserID,oldAcc,newAcc);
			this.OnExternalAccountUpdate();

		}
		
		public ExternalAccount GetExternalAccountFromName(string extAccName)
		{
			for(int i=0; i< this.ExternalAccountList.Count;i++)
			{
				if(((ExternalAccount)ExternalAccountList[i]).AccountName.Equals( extAccName))
				{
					//UpdateExternalAccount(transaction,((ExternalAccount)ExternalAccountList[i]));
					return (ExternalAccount)ExternalAccountList[i];
				}
			}
			return null;
		}


		public void AddTransaction(Transaction transaction)
		{
			// TODO: validate the trasaction before sending to databse(wisam is making sure it is sent validated)

            uint ID;
            // Cross application boundary
            try
            {
                ID = remoteConnection.AddNewTransaction(this.currentUserID, transaction, this.GetExternalAccountFromName(transaction.SourceExternalAccount));
            }
            catch (System.Net.WebException e1)
            {
                onServerDisconnected();
                return;
            }

			transaction.TransactionID = ID;
			TransactionList.Add(transaction);

			// update the account balance
			UpdateExternalAccountBalance(transaction);
		
			NotifyOtherUsersOnTransaction(transaction);
			OnTransactionUpdate(); //raise the transaction modified event
			
		}
		public void ApproveTransaction(Transaction approvedTransaction)
		{
			//TransactionNotificationList.Remove(approvedTransaction);
			if (approvedTransaction.IsNotify == true)	// if the transaction had a notification
			{ // remove it
				remoteConnection.TransactionNotificationApproved(currentUserID, approvedTransaction.TransactionID);
			}

			approvedTransaction.IsNotify=false;
			
		}
		/// <summary>
		/// Modifies a transaction, and sends notifications if applicable.
		/// </summary>
		/// <param name="oldTransaction"></param>
		/// <param name="newTransaction"></param>
		public void ModifyTransaction(Transaction oldTransaction, Transaction newTransaction)
		{
			bool AcctChanged = !(oldTransaction.SourceExternalAccount.Equals(newTransaction.SourceExternalAccount));
			bool amtInvolvedChanged = !(oldTransaction.InvolvedUserAmount(currentUserID.Name).Equals(newTransaction.InvolvedUserAmount(currentUserID.Name)));
			bool noNotifyableChanges = oldTransaction.EqualsIgnoreAccount(newTransaction);

			if (AcctChanged || !noNotifyableChanges)
			{
				newTransaction.TransactionID =oldTransaction.TransactionID;
				remoteConnection.UpdateTransaction(currentUserID,newTransaction,oldTransaction);
				
				if (AcctChanged || amtInvolvedChanged)	// if the account balances need updating
				{
					if (!oldTransaction.SourceExternalAccount.Equals(""))	// if the previous external account was defined
					{
						revertExternalAccountBalance(oldTransaction);
					}
					UpdateExternalAccountBalance(newTransaction);
				}
				
			}

			if(!noNotifyableChanges)
			{
				NotifyOtherUsersOnTransaction( newTransaction);				
			}

			// the transaction that is modified by current user is approved by current user
			ApproveTransaction(oldTransaction);

			// refresh our lists
			TransactionList.Remove(oldTransaction);
			TransactionList.Add(newTransaction);

			this.OnTransactionUpdate();
		}

		/// <summary>
		/// notify all but current user
		/// </summary>
		/// <param name="newTransaction"></param>
		public void NotifyOtherUsersOnTransaction(Transaction newTransaction)
		{
			// get users to notify
			
			ArrayList usersToNotify = newTransaction.getInvolvedUsers();
			if (usersToNotify.Contains(currentUserID.Name))
			{
				usersToNotify.Remove(currentUserID.Name);
			}
			remoteConnection.NotifyUsersOnTransaction(currentUserID, newTransaction.TransactionID, (string []) usersToNotify.ToArray(typeof(System.String)));
		}
	

		public UserIdentity CurrentUserID
		{
			get
			{
				return currentUserID;
			}
			set
			{
				currentUserID = value;
			}
		}
		public void DeleteTransaction(Transaction transaction)
		{
			
			remoteConnection.DeleteTransaction(this.currentUserID,transaction.TransactionID);
		}
		
       
		/// <summary>
		/// Attempts to login to the server. 
		/// </summary>	
		/// <exception cref="InvalidUserCredentialsException"></exception>	
		/// <param name="userName"></param>
		/// <param name="password"></param>
		public void Login(string userName, string password)
		{
			UserIdentity claimedUID = new UserIdentity(userName,password);

            // bug 123 fix: removed catch to System.Exception
				bool result;

                try
                {
                    result = remoteConnection.ValidateLogin(claimedUID);
                }
                catch (System.Net.WebException e1)
                {
                    onServerDisconnected();
                    return;
                }
                if (!result) throw new MizExceptions.InvalidUserCredentialsException();


			this.currentUserID=claimedUID; //store the uid
			GetAllUserTransactions();
			ExternalAccountList = remoteConnection.GetAllUserExternalAccounts(currentUserID);//get External Accounts
			//TransactionNotificationList = remoteConnection.GetTransactionsWithNotifications(currentUserID);
			//MergeTransactionLists();
		}

		/// <summary>
		/// get the users transactions from the database
		/// </summary>
		public void GetAllUserTransactions()
		{
			TransactionList = remoteConnection.GetAllUserTransactions(currentUserID);//get the users transaction(if any)
			ArrayList notifications = remoteConnection.GetNotifications(currentUserID);

			
			// for each transaction, update the notification flag.
			foreach (uint iNotification in notifications)
			{
				Transaction notifyTx = GetTransactionFromID(iNotification);
				if (notifyTx!=null)	// if there are notifications for transactions that do not exist, skip them
				{
					notifyTx.IsNotify=true;
				}
			}
		}

		/// <summary>
		/// Goes through all transactions which occured in the last 30 days
		/// Sums all expenses and revenues
		/// </summary>
		/// <returns>ExpRevPair structure</returns>
		public ExpRevPair GenerateExpRevReport()
		{
			DateTime today = new DateTime();
			ArrayList exprev = new ArrayList();

			ExpRevPair t = new ExpRevPair();
			t.expense = 0;
			t.revenue = 0;

			for (int i = 0; i < this.TransactionList.Count; i++)
			{
				Transaction temp = ((Transaction)this.TransactionList[i]);
				DateTime transDate = temp.Date;

				// Check if trans data within 30 days of today
				if (DateTime.Compare(DateTime.Now, transDate) <= DateTime.Compare(DateTime.Now, DateTime.Now.AddDays(-30)))
				{
					if (temp.Type.Equals("Expense"))
					{
						t.expense += temp.InvolvedUserAmount(this.currentUserID.Name);
					}

					else if (temp.Type.Equals("Revenue"))
					{
						t.revenue += temp.InvolvedUserAmount(this.currentUserID.Name);
					}
				}
			}

			return t;
		}


		/// <summary>
		/// Generates an expense vs revenue reports which starts from the given date, until today
		/// </summary>
		/// <param name="beginDate"></param>
		/// <returns></returns>
		public ExpRevPair[] GenerateExpRevReport(DateTime beginDate)
		{
			DateTime endDate = DateTime.Now;

			return GenerateExpRevReport(beginDate,endDate);

		}



		/// <summary>
		/// generates an expense vs revenue report which starts from a set number of days in the past, until now.
		/// </summary>
		/// <param name="numDaysAgo"></param>
		/// <returns></returns>
		public ExpRevPair[] GenerateExpRevReport(int numDaysAgo)
		{
			DateTime endDate = DateTime.Now;
			TimeSpan ts = new TimeSpan(numDaysAgo,0,0,0);
			DateTime beginDate = endDate -ts;

			return GenerateExpRevReport(beginDate,endDate);

		}

		/// <summary>
		/// generates an expense vs revenue report from two set dates
		/// </summary>
		/// <param name="beginDate"></param>
		/// <param name="endDate"></param>
		/// <returns></returns>
		public ExpRevPair[] GenerateExpRevReport(DateTime beginDate, DateTime endDate)
		{
			/**
			 * initialize the result to be the size of the date range
			 * go through each transaction			
			 *	if the date falls in between the time period
			 *		add the tx amount into expense or revenue location
			 * 
			 */
			
			TimeSpan diffTime = endDate - beginDate;
			int differenceInDays = diffTime.Days;// Difference in days.

			ExpRevPair[] result = new ExpRevPair[differenceInDays+1];

			// initialize
			for (int i=0;i<result.Length;i++)
			{
				result[i].expense=0;
				result[i].revenue=0;
			}

			foreach (Transaction iTx in this.TransactionList)	// for each transaction
			{
				if (iTx.Date >= beginDate && iTx.Date<=endDate)	// if the tx date is within range
				{
					TimeSpan tsBeginToTx = iTx.Date-beginDate;
					int index=tsBeginToTx.Days;	// find the day number
					if (iTx.Type.Equals("Revenue"))
					{
						result[index].revenue+=iTx.InvolvedUserAmount(currentUserID.Name);
					}
					else if (iTx.Type.Equals("Expense"))
					{
						result[index].expense+=iTx.InvolvedUserAmount(currentUserID.Name);
					}
				}
			}

			return result;
			
		}

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public UserAmountPair[] GenerateSharedTxReport()
        {
            /**
             * Go through each transaction
             *  go through each share amongst two people
             *      add or subtract that amount from our report
             */
            ArrayList userAmountPairReport = new ArrayList();
            foreach (Transaction iTx in this.TransactionList)	// for each transaction
            {
                ArrayList iTxReport = iTx.generateSummaryReport();
                foreach (ReportSharedTransaction iShare in iTxReport)
                {
                    if (iShare.giver.Equals(iShare.receiver) // if the giver and the receiver are the same person
                        || !iShare.IsInvolved(currentUserID.Name))   // or if neither the giver nor the receiver is me
                        continue;   // do not do anything in this case
                    
                    UserAmountPair reportItem = new UserAmountPair();
                    if (iShare.giver.Equals(currentUserID.Name))    // if i am the giver
                    {
                        // add reciever to the user amount pair with a negative value
                        reportItem.username = iShare.receiver;
                        reportItem.amount = -iShare.amount;
                    }

                    if (iShare.receiver.Equals(currentUserID.Name))
                    {
                        // add the giver to the user amount pair with a positive amount
                        reportItem.username = iShare.giver;
                        reportItem.amount = iShare.amount;
                    }

                    /**
                     * before adding this reportItem to the report, we check if we have a history
                     * with this user to which we can append the amount.
                     * */
                    bool found = false;
                    for (int i = 0; i < userAmountPairReport.Count; i++)
                    {
                        UserAmountPair iPair = (UserAmountPair)userAmountPairReport[i]; // the current user amount pair being searched
                    
                        if (iPair.username.Equals(reportItem.username))
                        {
                            iPair.amount += reportItem.amount;
                            found = true;
                            userAmountPairReport[i] = iPair;    //save the change back
                            break;
                        }
                    }
                    if (!found)// if we dont have a history with this user
                    {
                        userAmountPairReport.Add(reportItem);
                    }
                }
            }

            return (UserAmountPair[])userAmountPairReport.ToArray(typeof(UserAmountPair));

        }


			
		/*
		* design automatically flags  
		///
		/// <summary>
		/// this function will merge the two lists and set boolean modified to true for notificaion transactions 
		/// </summary>
		public void MergeTransactionLists()
		{
			if (this.TransactionList==null) return;
			for (int i =0;i < this.TransactionList.Count;i++)
			{
				for (int j =0;j<this.TransactionNotificationList.Count;j++) //Is it ok to Check with IDs or should be objects
				{
					if (((Transaction)TransactionList[i]).TransactionID == ((Transaction)TransactionNotificationList[j]).TransactionID)
						((Transaction)TransactionList[i]).IsNotify =true;
				}
			}
		}*/

		/// <summary>
		/// Chnage Users password on the database
		/// </summary>
		/// <param name="newPassord"></param>
		public void ChangePassword(string newPassword)
		{
			currentUserID.Password = newPassword;
			remoteConnection.ChangePassword(currentUserID);
		}

		/// <summary>
		/// Attempts to add a new user to the remote database.
		/// </summary>
		/// <exception cref="UserAlreadyExistsException"></exception>
		/// <param name="id"></param>Create a new identity and send it to the remote databse</param>
		public void AddNewUser(UserIdentity id)
		{
			// TODO: go through the entire user list and check if any of the users are new. if they are, create a new user

            try
            {
                remoteConnection.AddNewUser(id);
                remoteConnection.AddNewExternalAccount(id, new Cash("Cash", id.Name));
            }
            catch (System.Net.WebException)
            {
                onServerDisconnected();
            }
		}
		
		public void AddNewAccount(ExternalAccount acct)
		{
			for(int i=0; i< this.ExternalAccountList.Count;i++)
			{
				if(((ExternalAccount)ExternalAccountList[i]).AccountName == acct.AccountName)
				{
					throw new AccountAlreadyExistException();
				}
			}

			this.remoteConnection.AddNewExternalAccount(this.currentUserID,acct);
			this.ExternalAccountList = remoteConnection.GetAllUserExternalAccounts(this.currentUserID);
		}
		
		/**
		 * These functions are under construction
		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public string[] GetRecurrenceTypes()
		{
			foreach (item in RecurrenceRecord.TransactionType.  Enum.)
			{
				item.;
				Enum.
			}
			return null;// Enum.GetValues(typeof(RecurrenceRecord.RecurrencePattern));
		}

		public string[] GetTransactionTypes()
		{
			return null; Enum.GetValues(typeof(Transaction.TransactionType));
		}
		*/
		

		/// <summary>
		/// gets all the user names in the system
		/// </summary>
		/// <returns>An array of user names</returns>
		public string[] GetAllUserNames()
		{
			//returns a String array from the function, by type casting the Arraylist to a String type array
			string[] result;

            // cross application domain
            try
            {
                result = (string[])remoteConnection.GetUserNames().ToArray(typeof(string));
            }
            catch (System.Net.WebException e1)
            {
                onServerDisconnected();
                return new String[0];   // return something that wont make callers unhappy
            }
            return result;
		}



		/// <summary>
		/// Takes and enum and returns the description specified in its attribute
		/// </summary>
		/// <param name="en">The enum</param>
		/// <returns>string representation of the enum</returns>
		public static string GetEnumDescription(Enum en)
		{
			Type type = en.GetType();

			System.Reflection.MemberInfo[] memInfo = type.GetMember(en.ToString());

			if (memInfo != null && memInfo.Length > 0)

			{

				object[] attrs = memInfo[0].GetCustomAttributes(typeof(EnumDescription),
					false);

				if (attrs != null && attrs.Length > 0)

					return ((EnumDescription)attrs[0]).Text;

			}

			return en.ToString();
		}
		/// <summary>
		/// will return the transaction object whe asked for by its ID
		/// </summary>
		/// <param name="txID"></param>
		/// <returns></returns>
		public Transaction GetTransactionFromID(uint txID)
		{
			Transaction tmp = null;
			foreach (Transaction tx in this.TransactionList)
			{
				if (tx.TransactionID == txID) {
					tmp = tx;
					break;}
			}
			return tmp;
		}

		/// <summary>
		/// Raises the TransactionAction event
		/// </summary>
		private void OnTransactionUpdate()
		{
			if (this.TransactionAction!=null)
			{
				this.TransactionAction(this, new MizEventArgs.ActionTransactionUpdate());
			}
		}

        /// <summary>
        /// Raise the server disconnect event with default args.
        /// </summary>
        /// <exception cref="ServerUnavailableException">Unconditionally throws the exception</exception>
        private void onServerDisconnected()
        {
            if (this.ServiceDisconnectedAction != null)
            {
                this.ServiceDisconnectedAction(this, new MizEventArgs.ServiceDisconnectEventArgs());
            }
            throw new ServerUnavailableException();
        }

		/// <summary>
		/// raises the TransactionAction event
		/// </summary>
		private void OnExternalAccountUpdate()
		{
			if (this.ExternalAccountAction!=null)
			{
				this.ExternalAccountAction(this, new MizEventArgs.ActionExternalAccountUpdate());
			}
		}

		/// <summary>
		/// returns array of strings of account names
		/// </summary>
		/// <returns></returns>
		public string[] GetExternalAccountList()
		{
			string[] result=new string[ExternalAccountList.Count];
			int i=-1;
			foreach (ExternalAccount extAcc in this.ExternalAccountList)
			{
				i++;
				result[i] = extAcc.AccountName;

			}
			return result;
		}

		public string getUserLastLoggedInTime()
		{
			return remoteConnection.UserLastLoggedIn(currentUserID).ToString();
		}



	}

	

}
